﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web;
using System.Xml.XPath;
using Selenium;

namespace Quail
{
    /// <summary>
    /// Implementation of SimpleBrowser that uses Microsoft.Net.Http
    /// </summary>
    public class BasicWebBrowser : SimpleBrowser
    {
        private XPathNavigator _xml;
        private static bool _initialized = false;
        private readonly HttpClient _myclient;
        private bool _disposed;

        private readonly List<HttpTransaction> _transactions = new List<HttpTransaction>();

        private class HttpTransaction
        {
            public Uri Uri { get; set; }
            public BasicWebRequest Req { get; set; }
            public HttpResponseMessage Resp { get; set; }
        }


        /// <summary>
        /// Constructs a basic web browser instance
        /// </summary>
        /// <param name="testSession"></param>
        /// <param name="webpage"></param>
        public BasicWebBrowser(BasicTestSession testSession, BasicWebPage webpage)
            : base(testSession, webpage)
        {
            if (!_initialized)
            {
                _initialized = true;
                ServicePointManager.DefaultConnectionLimit = 48;
            }

            _myclient = new HttpClient
            {
                // default response buffer is 64K.  bump this up to ~50M
                MaxResponseContentBufferSize = 50000000,
                // Timeout = new TimeSpan(0, 0, 0, 0, Timeout)
            };

            Cookie = new BasicWebCookie(this);
            KeepCookiesBetweenRequests = true;
        }

        public delegate void OnAfterNavigateDelegate(BasicWebRequest webrequest, HttpResponseMessage currentResponse);
        public event OnAfterNavigateDelegate OnAfterNavigate;


        public delegate void OnBeforeNavigateDelegate(BasicWebRequest request, HttpClient webRequest);
        public event OnBeforeNavigateDelegate OnBeforeNavigate;

        /// <summary>
        /// Frees the http response object
        /// </summary>
        protected override void Dispose(bool disposing)
        {
            if (disposing && !_disposed)
            {
                _xml = null;

                _transactions
                    .Select(t => t.Resp)
                    .Where(resp => resp != null)
                    .ToList()
                    .ForEach(resp => resp.Dispose());

                _transactions.Clear();

                if (Cookie != null)
                {
                    var bwc = Cookie as BasicWebCookie;
                    Cookie = null;
                    if (bwc != null)
                        bwc.Dispose();
                }

                _myclient.Dispose();
                _disposed = true;
            }

            base.Dispose(disposing);
        }

        /// <summary>
        /// Gets the current BasicTestSession
        /// </summary>
        public BasicTestSession BasicTestSession
        {
            get { return (BasicTestSession) TestSession; }
        }

        /// <summary>
        /// Gets the Basic shell
        /// </summary>
        public BasicTestShell Shell
        {
            get { return GenericPage.Shell; }
        }

        /// <summary>
        /// Gets the default BasicWebPage
        /// </summary>
        public new BasicWebPage GenericPage
        {
            get { return base.GenericPage as BasicWebPage; }
            set { base.GenericPage = value; }
        }

        /// <summary>
        /// Gets an interface to Selenium or a Selenium-like implementation
        /// </summary>
        public override ISelenium SeleniumInterface
        {
            get { return BasicTestSession.SeleniumInterface; }
        }


        /// <summary>
        /// Gets or sets a value indicating whether to keep browser cookies between requests
        /// </summary>
        public bool KeepCookiesBetweenRequests { get; set; }

        /// <summary>
        /// Navigates to a url on the current site - use OffSiteNavigate to navigate to a differet web site
        /// </summary>
        /// <param name="request">A basic web request</param>
        public void Navigate(BasicWebRequest request)
        {
            Navigate(request, GenericPage);
        }

        /// <summary>
        /// Navigates to a url on the current site - use OffSiteNavigate to navigate to a differet web site
        /// </summary>
        /// <param name="request">A basic web request</param>
        /// <param name="page">The page to sync against after the navigation</param>
        /// <param name="ysodCheck">The YSOD check flag</param>
        public void Navigate(BasicWebRequest request, IWebPage page, YSODCheckTypes ysodCheck = YSODCheckTypes.CheckForYSOD)
        {
            var task = ActiveTask.BeginTask("BasicWebBrowser.Navigate");
            try
            {
                var uri = request.GetUri(TestSession.BaseUrl);

                task.AddAttribute("NavigateTo", uri.AbsoluteUri);

                PerformNavigate(request);

                page.Sync(ysodCheck);
                task.Finish();
            }
            catch (Exception e)
            {
                task.Finish(e);
                throw;
            }
        }

        /// <summary>
        /// Gets the XPathNavigator for the current response
        /// </summary>
        /// <returns></returns>
        public XPathNavigator GetXml()
        {
            if (_xml == null)
            {
                // &nbsp; shows up in some of these pages but is not xhtml compliant.  get rid of it to avoid parsing errors.
                using (var reader = new StringReader(GetCurrentResponse().Replace("&nbsp;", "&#160;")))
                    _xml = new XPathDocument(reader).CreateNavigator();
            }

            return _xml;
        }

        /// <summary>
        /// Navigates to a url on the current site - use OffSiteNavigate to navigate to a differet web site
        /// </summary>
        /// <param name="url">A url local to the current site</param>
        /// <param name="ysodCheck">The YSOD check flag</param>
        public void Put(string url,  YSODCheckTypes ysodCheck = YSODCheckTypes.CheckForYSOD)
        {
            var originalurl = url;
            ActiveTask
                .RunTask(
                    "SimpleBrowser.Put",
                    task =>
                    {
                        task.AddAttribute("PutTo", originalurl);
                        var uri = new Uri(url, UriKind.RelativeOrAbsolute);

                        if (!uri.IsAbsoluteUri)
                        {
                            var baseuri = new Uri(TestSession.BaseUrl, UriKind.Absolute);
                            var baseurl = baseuri.GetLeftPart(UriPartial.Path);

                            if (!baseurl.EndsWith("/") && !url.StartsWith("/"))
                                url = baseurl + "/" + url;
                            else if (baseurl.EndsWith("/") && url.StartsWith("/"))
                                url = baseurl + url.Substring(1);
                            else
                                url = baseurl + url;
                        }

                        var request = new BasicWebRequest { Url = url, Method = WebRequestMethods.Http.Put };
                        PerformNavigate(request);

                        if (SilentLogging)
                            task.Rollback();
                    });
        }

        /// <summary>
        /// Begins navigation request
        /// </summary>
        /// <param name="request"></param>
        private void PerformNavigate(BasicWebRequest request)
        {
            var uri = request.GetUri(TestSession.BaseUrl);

            _xml = null;

            var tran = new HttpTransaction
                           {
                               Uri = uri,
                               Req = request
                           };

            AddTransaction(tran);

            if (OnBeforeNavigate != null)
                OnBeforeNavigate(request, _myclient);

            var response = new Task<HttpResponseMessage>(HttpClientTask, tran);

            response.ContinueWith(t => RequestComplete(t));
            response.Start();
        }

        private HttpResponseMessage HttpClientTask(object tran)
        {
            try
            {
                var thistrans = (HttpTransaction)tran;

                // now decide if it's a post
                switch (thistrans.Req.Method)
                {
                    case WebRequestMethods.Http.Get:
                        return _myclient.Get(thistrans.Uri);
                    case WebRequestMethods.Http.Post:
                        return _myclient.Post(thistrans.Uri, thistrans.Req.GetContent());
                    case WebRequestMethods.Http.Put:
                        return _myclient.Put(thistrans.Uri, thistrans.Req.GetContent());
                    case "DELETE":
                        // for whatever reason, the WebRequestMethods.Http class does not contain a "Delete" field (http://msdn.microsoft.com/en-us/library/system.net.webrequestmethods.http.aspx)
                        return _myclient.Delete(thistrans.Uri);
                    default:
                        throw new Exception("Invalid Request Method provided: " + thistrans.Req.Method);
                }
            }
            catch (Exception e)
            {
                return new HttpResponseMessage(HttpStatusCode.ServiceUnavailable, e.Message);
            }
        }

        /// <summary>
        /// Gets the current HttpStatusCode for the response (NULL if no response is available)
        /// </summary>
        public HttpStatusCode? CurrentStatusCode
        {
            get
            {
                if (RawResponse != null)
                    return RawResponse.StatusCode;
                return null;
            }
        }

        /// <summary>
        /// Gets a value indicating whether there is still a request pending
        /// </summary>
        public bool RequestPending
        {
            get { 
                var tran = CurrentTransaction;
                if (tran == null)
                    throw new NotSupportedException("Oops - no current transaction when calling RequestPending!  [this shouldn't happen]");

                return (tran.Req != null && tran.Resp == null);
            }
        }

        private HttpTransaction CurrentTransaction
        {
            get { return _transactions.FirstOrDefault(); }
        }

        private void RemoveTransaction(HttpTransaction tran)
        {
            lock (_transactions)
            {
                _transactions.Remove(tran);
            }
        }

        private void AddTransaction(HttpTransaction tran)
        {
            lock (_transactions)
            {
                _transactions.Insert(0, tran);

                while (_transactions.Count > 20)
                    _transactions.RemoveAt(_transactions.Count - 1);
            }
        }

        private void PopTransaction()
        {
            lock (_transactions)
            {
                if (_transactions.Count > 1)
                    _transactions.RemoveAt(0);
                else
                    throw new Exception("Unable to perform Back() operation - no pages to go back to!");
            }
        }

        /// <summary>
        /// Gets the raw response object
        /// </summary>
        public HttpResponseMessage RawResponse
        {
            get
            {
                return _transactions.Count == 0
                           ? null
                           : _transactions[0].Resp;
            }
        }

        /// <summary>
        /// Gets the raw request object
        /// </summary>
        public BasicWebRequest RawRequest
        {
            get
            {
                return _transactions.Count == 0
                           ? null
                           : _transactions[0].Req;
            }
        }

        /// <summary>
        /// Returns the content type of the string
        /// </summary>
        public string CurrentContentType
        {
            get
            {
                if (RawResponse != null)
                    return RawResponse.Content.Headers.ContentType.MediaType;
                return null;
            }
        }

        /// <summary>
        /// Gets the current HTTP response content as a string
        /// </summary>
        /// <returns></returns>
        public string GetCurrentResponse()
        {
            var task = ActiveTask.BeginTask("GetCurrentResponse");
            try
            {
                var myResponse = RawResponse;
                if (myResponse == null)
                {
                    task.AddLog("No response");
                    return null;
                }

                var str = myResponse.Content == null ? string.Empty : myResponse.Content.ReadAsString();

                task.Rollback();

                return str;
            }
            catch (Exception e)
            {
                task.Finish(e);
                throw;
            }
        }



        #region "XML Parsing"

        /// <summary>
        /// Returns the XmlRoot node
        /// </summary>
        public XPathNavigator XmlRoot
        {
            get { return GetXml(); }
        }

        /// <summary>
        /// Get a node from the XML based on the given selector
        /// </summary>
        /// <param name="node">The XML in which to look for the selector</param>
        /// <param name="selector">The XML selector</param>
        /// <returns>The node matching the given selector in the XML</returns>
        public XPathNavigator GetNode(XPathNavigator node, string selector)
        {
            return node.SelectSingleNode(selector);
        }

        /// <summary>
        /// Get a node from the XML based on the given selector
        /// </summary>
        /// <param name="selector">The XML selector</param>
        /// <returns>The node matching the given selector in the XML</returns>
        public XPathNavigator GetNode(string selector)
        {
            return GetNode(XmlRoot, selector);
        }

        /// <summary>
        /// Get a node from the XML based on the given selector
        /// </summary>
        /// <param name="selector">The XML selector</param>
        /// <returns>The node matching the given selector in the XML</returns>
        public IEnumerable<XPathNavigator> GetNodes(string selector)
        {
            return GetNodes(XmlRoot, selector);
        }

        /// <summary>
        /// Get a node from the XML based on the given selector
        /// </summary>
        /// <param name="node">The XML in which to look for the selector</param>
        /// <param name="selector">The XML selector</param>
        /// <returns>The node matching the given selector in the XML</returns>
        public IEnumerable<XPathNavigator> GetNodes(XPathNavigator node, string selector)
        {
            return node.Select(selector)
                .Cast<XPathNavigator>();
        }

        /// <summary>
        /// Get a value from the XML based on the given selector
        /// </summary>
        /// <typeparam name="T">The type of the value to get</typeparam>
        /// <param name="node">The XML in which to look for the selector</param>
        /// <param name="selector">The XML selector</param>
        /// <returns>The value of the given selector in the XML</returns>
        public T GetValue<T>(XPathNavigator node, string selector)
        {
            return GetValue<T>(node.SelectSingleNode(selector));
        }

        /// <summary>
        /// Get a value from the XML based on the given selector
        /// </summary>
        /// <typeparam name="T">The type of the value to get</typeparam>
        /// <param name="selector">The XML selector</param>
        /// <returns>The value of the given selector in the XML</returns>
        public T GetValue<T>(string selector)
        {
            return GetValue<T>(XmlRoot, selector);
        }

        /// <summary>
        /// Get a value from the XML of the given node
        /// </summary>
        /// <typeparam name="T">The type of the value to get - string, int, decimal or bool</typeparam>
        /// <param name="node">The XML node</param>
        /// <returns>The value of the given selector in the XML</returns>
        public T GetValue<T>(XPathNavigator node)
        {
            if (node == null)
                return default(T);

            var val = node.Value;

            if (typeof(T).Equals(typeof(string)))
                return (T)(object)val;

            if (string.IsNullOrEmpty(val))
                return default(T);

            if (typeof(T).Equals(typeof(int)) || typeof(T).Equals(typeof(int?)))
                return (T)(object)Convert.ToInt32(val);
            if (typeof(T).Equals(typeof(decimal)) || typeof(T).Equals(typeof(decimal?)))
                return (T)(object)Convert.ToDecimal(val);
            if (typeof(T).Equals(typeof(double)) || typeof(T).Equals(typeof(double?)))
                return (T)(object)Convert.ToDouble(val);
            if (typeof(T).Equals(typeof(float)) || typeof(T).Equals(typeof(float?)) || typeof(T).Equals(typeof(Single)) || typeof(T).Equals(typeof(Single?)))
                return (T)(object)Convert.ToSingle(val);
            if (typeof(T).Equals(typeof(bool)) || typeof(T).Equals(typeof(bool?)))
                return (T)(object)Convert.ToBoolean(val);
            if (typeof(T).Equals(typeof(DateTime)) || typeof(T).Equals(typeof(DateTime?)))
                return (T) (object) Convert.ToDateTime(val);

            throw new Exception("Uknown type passed to GetValue, you should add some code to BasicWebPage.GetValue to handle type: " + typeof(T).FullName);
        }
        /*
        /// <summary>
        /// Get a list of items in the given navigator that match the given selector
        /// </summary>
        /// <typeparam name="T">The type of items to add to the list</typeparam>
        /// <param name="node">The containing xml navigator</param>
        /// <param name="selector">The selector used to locate items</param>
        /// <param name="additem">A function to convert each item from xml to the given type for addition to the list</param>
        /// <returns>The list of items</returns>
        public List<T> GetList<T>(XPathNavigator node, string selector, Func<XPathNavigator, T> additem)
        {
            return GetNodes(node, selector)
                .Select(additem)
                .ToList();
        }

        /// <summary>
        /// Get a list of items that match the given selector
        /// </summary>
        /// <typeparam name="T">The type of items to add to the list</typeparam>
        /// <param name="selector">The selector used to locate items</param>
        /// <param name="additem">A function to convert each item from xml to the given type for addition to the list</param>
        /// <returns>The list of items</returns>
        public List<T> GetList<T>(string selector, Func<XPathNavigator, T> additem)
        {
            return GetList(XmlRoot, selector, additem);
        }
         * */

        /// <summary>
        /// Get a list of string values found with the given xml element name
        /// </summary>
        /// <param name="node">The containing navigator</param>
        /// <param name="selector">The XPath selector</param>
        /// <returns>The list of strings</returns>
        public List<T> GetValues<T>(XPathNavigator node, string selector)
        {
            return GetNodes(node, selector)
                .Select(GetValue<T>)
                .ToList();
        }

        #endregion


        /// <summary>
        /// Executed when the Http request has completed
        /// </summary>
        /// <param name="httptask"></param>
        protected void RequestComplete(Task<HttpResponseMessage> httptask)
        {
            // var task = shell.ActiveTask.BeginTask("BasicWebBrowser.RequestComplete");
            try
            {
                var tran = httptask.AsyncState as HttpTransaction;
                var curtran = CurrentTransaction;
                if (tran == null || tran != curtran)
                {
                    RemoveTransaction(tran);

                    // httptask.Result.Dispose();
                    httptask.Dispose();

                    if (tran == null)
                        throw new Exception("AsyncState is not a HttpTransaction - it should be!");
                    if (tran != curtran)
                        throw new Exception("webrequest != CurrentTransaction");

                    throw new Exception("Invalid state.  This should never happen");
                }

                // task.AddAttribute("Status", httptask.Result.StatusCode.ToString());

                if (OnAfterNavigate != null)
                    OnAfterNavigate(tran.Req, httptask.Result);

                // should I be disposing of the task?
                // httptask.Dispose(); // ?
                // task.Finish();

                _xml = null;
                tran.Resp = httptask.Result;
            }
            catch (Exception e)
            {
                Shell.ActiveTask.AddLog(LogStatus.Fail, "HttpResponseFail: " + e.Message);
            }
        }

        protected override string GetLocation()
        {
            var resp = RawResponse;
            if (resp == null)
                return string.Empty;

            if (resp.RequestMessage != null)
                return resp.RequestMessage.RequestUri.AbsoluteUri;

            if (RawRequest == null)
                return string.Empty;

            return RawRequest.GetUri().ToString();
        }

        protected override void PerformBack()
        {
            PopTransaction();
            _xml = null;
            var tran = CurrentTransaction;
            if (OnAfterNavigate != null)
                OnAfterNavigate(tran.Req, tran.Resp);
        }

        protected override void PerformRefresh()
        {
            var req = RawRequest;
            if (req == null)
                throw new Exception("Unable to refresh.  No current request is available.");

            PerformNavigate(req);
        }

        protected override string PerformGetUrlContent(string url)
        {
            PerformNavigate(url);
            return GetCurrentResponse();
        }

        protected override void PerformNavigate(string url)
        {
            PerformNavigate(new BasicWebRequest { Url = url });
        }

        private class BasicWebCookie : IWebCookie, IDisposable
        {
            private readonly BasicWebBrowser _browser;
            private readonly HttpCookieCollection _cookies = new HttpCookieCollection();

            public void Dispose()
            {
                _browser.OnAfterNavigate -= ParseCookies;
            }

            public BasicWebCookie(BasicWebBrowser browser)
            {
                _browser = browser;
                _browser.OnAfterNavigate += ParseCookies;
            }

            private void ParseCookies(BasicWebRequest webrequest, HttpResponseMessage currentresponse)
            {
                IEnumerable<string> values;

                if (currentresponse.Headers.TryGetValues("Set-Cookie", out values))
                {
                    foreach (var cookieText in values)
                    {
                        var bits = cookieText.Split(new[] {';'}, StringSplitOptions.RemoveEmptyEntries);

                        if (bits.Length == 0)
                            continue;

                        var cnameval = bits[0].Split('=');

                        var cname = cnameval[0].Trim();
                        var cvalue = cnameval[1].Trim();
                        HttpCookie cookie = _cookies[cname];

                        if (cookie != null)
                            _cookies.Remove(cname);

                        var expired = false;

                        cookie = new HttpCookie(cname, cvalue);

                        for (var cookiepartidx = 1; cookiepartidx < bits.Length; cookiepartidx++)
                        {
                            var cookiebit = bits[cookiepartidx].Trim().Split('=');
                            var bitname = cookiebit[0].Trim();
                            if (string.Compare(bitname, "expires", StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                cookie.Expires = DateTime.Parse(cookiebit[1]);
                                if (cookie.Expires < DateTime.Now)
                                {
                                    expired = true;
                                    break;
                                }
                            }
                            else if (string.Compare(bitname, "path", StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                cookie.Path = cookiebit[1];
                            }
                            else if (string.Compare(bitname, "domain", StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                cookie.Domain = cookiebit[1];
                            }
                            else if (string.Compare(bitname, "httponly", StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                cookie.HttpOnly = true;
                            }
                            else if (string.Compare(bitname, "secure", StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                cookie.Secure = true;
                            }
                        }

                        if (!expired)
                            _cookies.Add(cookie);
                    }
                }
            }

            public HttpCookieCollection GetCookies()
            {
                return _cookies;
            }

            public bool DoesCookieExist(string name)
            {
                var c = _cookies[name];
                return c != null;
            }

            public string GetCookieValue(string name)
            {
                var c = _cookies[name];
                if (c == null)
                    return null;

                return c.Value;
            }

            public void DeleteCookie(string name)
            {
                _cookies.Remove(name);
            }
        }
    }
}
